package com.harboursoftware.xstorage.web;

import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.collect.Multimap;
import com.harboursoftware.xstorage.Constants;
import com.harboursoftware.xstorage.XStorageException;
import com.harboursoftware.xstorage.XStorageRuntimeException;
import com.harboursoftware.xstorage.db.DaoManager;
import com.harboursoftware.xstorage.db.UserDao;
import com.harboursoftware.xstorage.model.XBucket;
import com.harboursoftware.xstorage.model.XObject;
import com.harboursoftware.xstorage.model.XUser;
import com.harboursoftware.xstorage.results.ErrorResult;
import com.harboursoftware.xstorage.util.MultimapUtils;

class AuthorizationChecker {
    private final static Logger log = LoggerFactory.getLogger(AuthorizationChecker.class);

    private static UserDao userDao = DaoManager.getInstance().getUserDao();

    /**
     * TODO: 该方法名字不能表达方法的作用，是否应该用抛出异常来替代 返回null，通过签名校验当前用户的请求是否合法（签名和请求时间）， 如果合法返回当前请求者。如果请求的、头中不包括Authorization，
     * 返回null。 for more details,refer to
     * http://docs.amazonwebservices.com/AmazonS3/2006-03-01/dev/index.html?UsingHTTPPOST.html.
     * 在发生异常的情况下，抛出的XStorageException可以通过getErrorResult()方法回去具体的错误Result。
     * 
     * @return 如果处在签名并且签名正确时返回请求者，如果不存在签名则返回null
     * @throws XStorageException 签名错误或不存在该请求者的记录
     */
    public static XUser check(String method, XBucket bucket, XObject object, Multimap<String, String> headers,
        Map<String, String> parameters) throws XStorageException {

        // 1.检测x-amz-date,Date,Expires是否过期（存在的情况下）, x-amz-date存在的情况下需要清空Date，
        // 拥有Expires的是PutObject, GetObject, PostObject
        // 2.从DB中获取Owner
        // 3.使用Owner密匙加密request
        // 3.比较请求带上来的和计算出来的签名是否一致

        // 签名的携带有两中1.header 2.paramter
        // 中的Expires(PostObject,GetObject)其中GetObject可以同时使用这两张签名携带的方式

        AccessKeyIdAndSign accessKeyIdAndSign = parseAccessKeyIdAndSign(method, headers, parameters);

        if (accessKeyIdAndSign == null) {
            // TODO 考虑是否返回XUser.anonymous
            return null;
        }

        XUser requester = findRequesterByAccessKeyId(accessKeyIdAndSign.accessKeyId);
        String signatureProvided = accessKeyIdAndSign.sign;
        String signatureCalculated =
            calculateSignature(requester.getSecretAccessKey(), method, bucket, object, headers, parameters);
        compareSignatrue(signatureProvided, signatureCalculated);

        return requester;
    }

    private static AccessKeyIdAndSign parseAccessKeyIdAndSign(String method, Multimap<String, String> headers,
        Map<String, String> parameters) throws XStorageException {
        if ("POST".equals(method)) {
            return parseFromPostParameters(parameters);
        } else if ("GET".equals(method) && parameters.containsKey("Signature")) {
            // TODO:处理存在Signature，但不存在其他参数的情况
            // Using Query String Authentication
            return parseFromGetParameters(parameters);
        } else {
            return parseFromHeaders(headers);
        }
    }
    
    /**
     * refer to <a
     * href="http://docs.amazonwebservices.com/AmazonS3/2006-03-01/dev/S3_Authentication.html">Authentication</a>
     * 
     * @throws XStorageSecurityException
     */
    private static AccessKeyIdAndSign parseFromHeaders(Multimap<String, String> headers) throws XStorageException {
        // TODO : authorization 是否要移到 contants类中
        String authorization = MultimapUtils.getFirstValue(headers, "authorization");
        // TODO : Query String Request Authentication Alternative 和Post的认证
        if (authorization == null) {
            return null;
        }

        // Authorization format : Type + " " + AwsAccessKeyId + ":" + signature
        String[] splitedAuthorization = StringUtils.split(authorization, ' ');
        if (splitedAuthorization.length != 2) {
            ErrorResult errorResult = ErrorResult.create(ErrorResult.Type.INVALID_ARGUMENT);
            errorResult.setMessage("Authorization header is invalid -- one and only one ' ' (space) required");
            errorResult.addOtherInfo("ArgumentName", "Authorization");
            errorResult.addOtherInfo("ArgumentValue", authorization);
            throw new XStorageException(errorResult);
        }

        String type = splitedAuthorization[0];
        if (!"AWS".equals(type)) {
            ErrorResult errorResult = ErrorResult.create(ErrorResult.Type.INVALID_ARGUMENT);
            errorResult.setMessage("Unsupported Authorization Type");
            errorResult.addOtherInfo("ArgumentName", "Authorization");
            errorResult.addOtherInfo("ArgumentValue", authorization);
            throw new XStorageException(errorResult);
        }

        splitedAuthorization = StringUtils.split(splitedAuthorization[1], ':');
        if (splitedAuthorization.length != 2) {
            ErrorResult errorResult = ErrorResult.create(ErrorResult.Type.INVALID_ARGUMENT);
            errorResult.setMessage("AWS authorization header is invalid.  Expected AwsAccessKeyId:signature");
            errorResult.addOtherInfo("ArgumentName", "Authorization");
            errorResult.addOtherInfo("ArgumentValue", authorization);
            throw new XStorageException(errorResult);
        }

        String accessKeyId = splitedAuthorization[0];
        String signatureProvided = splitedAuthorization[1];

        return new AccessKeyIdAndSign(accessKeyId, signatureProvided);
    }

    /**
     * refer to <a href="http://docs.amazonwebservices.com/AmazonS3/2006-03-01/dev/S3_QSAuth.html">Query String
     * Authentication</a> and <a
     * href="http://docs.amazonwebservices.com/AmazonS3/2006-03-01/API/RESTObjectPOST.html">Post Object</a>
     * 
     */
    private static AccessKeyIdAndSign parseFromGetParameters(Map<String, String> parameters) {
        // TODO:处理处在Signature，但不存在Signature的情况
        AccessKeyIdAndSign accessKeyIdAndSign = new AccessKeyIdAndSign();
        accessKeyIdAndSign.accessKeyId = parameters.get("AWSAccessKeyId");
        accessKeyIdAndSign.sign = parameters.get("Signature");
        return accessKeyIdAndSign;
    }

    private static AccessKeyIdAndSign parseFromPostParameters(Map<String, String> parameters) {
        AccessKeyIdAndSign accessKeyIdAndSign = new AccessKeyIdAndSign();
        accessKeyIdAndSign.accessKeyId = parameters.get("AWSAccessKeyId");
        accessKeyIdAndSign.sign = parameters.get("signature");
        return accessKeyIdAndSign;
    }

    

    private static XUser findRequesterByAccessKeyId(String accessKeyId) throws XStorageException {
        XUser requester = userDao.findUserByAccessKeyId(accessKeyId);

        if (requester == null) {
            ErrorResult errorResult = ErrorResult.create(ErrorResult.Type.INVALID_ACCESS_KEY_ID);
            errorResult.addOtherInfo("AWSAccessKeyId", accessKeyId);
            throw new XStorageException(errorResult);
        }

        return requester;
    }

    private static String calculateSignature(String secretAccessKey, String method, XBucket bucket, XObject object,
        Multimap<String, String> headers, Map<String, String> parameters) {
        if ("POST".equals(method)) {
            String policyString = parameters.get("policy");
            return sign(secretAccessKey, policyString);
        }
        String stringToSign = createStringToSign(method, bucket, object, headers, parameters);
        return sign(secretAccessKey, stringToSign);
    }

    /**
     * @throws XStorageSecurityException
     *             when signatures doesn't match
     */
    private static void compareSignatrue(String signatureProvided, String signatureCalculated) throws XStorageException {
        if (!signatureProvided.equals(signatureCalculated)) {
            log.debug("signatures doesn't match,the signature provided is [{}], the signature calculated is [{}]",
                signatureProvided, signatureCalculated);
            ErrorResult errorResult = ErrorResult.create(ErrorResult.Type.SIGNATURE_DOES_NOT_MATCH);
            errorResult.addOtherInfo("SignatureProvided", signatureProvided);
            throw new XStorageException(errorResult);
            // TODO 少了一个otherInfo
        }
        log.debug("signatures match,the provided is [{}],the calculated is [{}]", signatureProvided,
            signatureCalculated);
    }



    private static String createStringToSign(String method, XBucket bucket, XObject object,
        Multimap<String, String> headers, Map<String, String> parameters) {
        StringBuilder strBuildre = new StringBuilder();
        strBuildre.append(method.toUpperCase());
        strBuildre.append("\n");
        strBuildre.append(StringUtils.defaultString(MultimapUtils.getFirstValue(headers, "content-md5"), "").trim());
        strBuildre.append("\n");
        strBuildre.append(StringUtils.defaultString(MultimapUtils.getFirstValue(headers, "content-type"), "").trim());
        strBuildre.append("\n");
        strBuildre.append(StringUtils.defaultString(MultimapUtils.getFirstValue(headers, "date"), "").trim());
        strBuildre.append("\n");
        String canonicalizedAmzHeaders = createCanonicalizedAmzHeaders(headers);
        String canonicalizedResource = createCanonicalizedResource(bucket, object, parameters);
        strBuildre.append(canonicalizedAmzHeaders);
        strBuildre.append(canonicalizedResource);
        return strBuildre.toString();
    }

    private static String createCanonicalizedAmzHeaders(Multimap<String, String> headers) {
        Set<String> headerNames = new TreeSet<String>(headers.keySet());
        StringBuilder strBuilder = new StringBuilder();
        for (String headerName : headerNames) {
            // if starts with "x-amz-"
            if (headerName.startsWith(Constants.REST_HEADER_PREFIX)) {
                strBuilder.append(headerName.trim());
                strBuilder.append(":");
                Iterator<String> valueIterator = headers.get(headerName).iterator();
                while(valueIterator.hasNext()) {
                    strBuilder.append(valueIterator.next().trim());
                    if(valueIterator.hasNext()) {
                        strBuilder.append(",");
                    }
                }
                strBuilder.append("\n");
            }
        }
        return strBuilder.toString();
    }

    private static String createCanonicalizedResource(XBucket bucket, XObject object, Map<String, String> parameters) {
        StringBuilder strBuilder = new StringBuilder();
        strBuilder.append("/");
        if (bucket != null) {
            strBuilder.append(bucket.getName());
        }
        if (object != null) {
            strBuilder.append(object.getKey());
        } else if (object == null && bucket != null) {
            strBuilder.append("/");
        }

        String[] subResources = new String[] { "acl", "torrent", "logging", "location", "requestPayment" };

        for (String subresource : subResources) {
            if (parameters.containsKey(subresource)) {
                strBuilder.append("?");
                strBuilder.append(subresource);
                break;
            }
        }
        return strBuilder.toString();
    }

    // TODO : 是否需要把该类中的 关于安全的代码迁移到security包中
    private static String sign(String secretAccessKey, String stringToSign) {
        byte[] digest = signWithHmacSha1(secretAccessKey, stringToSign);
        return encodeBase64(digest);
    }

    /**
     * Calculate the HMAC/SHA1 on a string.
     * 
     * @param key
     * 
     * @param message
     * 
     * @return Signature
     * @throws XStorageRuntimeException
     */
    private static byte[] signWithHmacSha1(String key, String message) {
        // The following HMAC/SHA1 code for the signature is taken from the
        // AWS Platform's implementation of RFC2104
        // (amazon.webservices.common.Signature)
        //
        // Acquire an HMAC/SHA1 from the raw key bytes.
        try {
            SecretKeySpec signingKey =
                new SecretKeySpec(key.getBytes(Constants.DEFAULT_ENCODING), Constants.HMAC_SHA1_ALGORITHM);
            Mac mac = Mac.getInstance(Constants.HMAC_SHA1_ALGORITHM);
            mac.init(signingKey);
            return mac.doFinal(message.getBytes(Constants.DEFAULT_ENCODING));
        } catch (UnsupportedEncodingException e) {
            throw new XStorageRuntimeException("Unable to get bytes from secret string", e);
        } catch (NoSuchAlgorithmException e) { // should not happen
            throw new XStorageRuntimeException("Could not find sha1 algorithm", e);
        } catch (InvalidKeyException e) { // also should not happen
            throw new XStorageRuntimeException("Could not initialize the MAC algorithm", e);
        }
    }

    private static String encodeBase64(byte[] digest) {
        byte[] b64 = Base64.encodeBase64(digest);
        return new String(b64);
    }
    
    /**
     * represent the accessKeyId and signature
     * 
     * @see #parseAccessKeyIdAndSignFromHeaders
     * @see #parseAccessKeyIdAndSignFromParameters
     */
    private static class AccessKeyIdAndSign {
        AccessKeyIdAndSign() {
        }

        AccessKeyIdAndSign(String accessKeyId, String sign) {
            this.accessKeyId = accessKeyId;
            this.sign = sign;
        }

        String accessKeyId;
        String sign;
    }
}
